<!--
@license
Copyright 2017 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<link rel="import" href="../iron-icon/iron-icon.html" />
<link rel="import" href="../paper-button/paper-button.html" />
<link rel="import" href="../paper-dropdown-menu/paper-dropdown-menu.html" />
<link rel="import" href="../paper-input/paper-input.html" />
<link rel="import" href="../paper-input/paper-textarea.html" />
<link rel="import" href="../paper-item/paper-item.html" />
<link rel="import" href="../paper-listbox/paper-listbox.html" />
<link rel="import" href="../paper-progress/paper-progress.html" />
<link rel="import" href="../paper-spinner/paper-spinner-lite.html" />
<link rel="import" href="../paper-toast/paper-toast.html" />
<link rel="import" href="../tf-imports/polymer.html" />
<link rel="import" href="../tf-backend/tf-backend.html" />
<link rel="import" href="../tf-dashboard-common/dashboard-style.html" />
<link rel="import" href="../tf-dashboard-common/tf-dashboard-layout.html" />
<link rel="import" href="../tf-graph/tf-graph.html" />
<link rel="import" href="../tf-graph-loader/tf-graph-loader.html" />
<link rel="import" href="../tf-imports/lodash.html" />
<link rel="import" href="../tf-runs-selector/tf-runs-selector.html" />
<link rel="import" href="../tf-tensorboard/registry.html" />
<link rel="import" href="health-pills.html" />
<link rel="import" href="tf-debugger-continue-dialog.html" />
<link rel="import" href="tf-debugger-initial-dialog.html" />
<link rel="import" href="tf-debugger-resizer.html" />
<link rel="import" href="tf-op-selector.html" />
<link rel="import" href="tf-session-runs-view.html" />
<link rel="import" href="tf-source-code-view.html" />
<link rel="import" href="tf-tensor-data-summary.html" />
<link rel="import" href="tf-tensor-value-multi-view.html" />

<dom-module id="tf-debugger-dashboard">
  <template>
    <paper-toast id="toast" text="" always-on-top></paper-toast>
    <tf-debugger-initial-dialog id="initialDialog"></tf-debugger-initial-dialog>
    <!-- TODO(caisq, chihuahua): This shouldn't be a dialog. It should be a prettier
      widget that supports showing both text, curves and images. -->
    <tf-dashboard-layout>
      <div class="sidebar" slot="sidebar" id="left-pane">
        <div id="node-entries" class="node-entries">
          <div class="debugger-section-title">Runtime Node List</div>
          <div class="toggle-source-code">
            Show Code:
            <paper-toggle-button
              class="toggle-source-code"
              id="show-source-code"
              checked="{{_sourceCodeShown}}"
            ></paper-toggle-button>
          </div>
          <tf-op-selector
            debug-watches="[[_debugWatches]]"
            debug-watch-change="[[_createDebugWatchChangeHandler()]]"
            node-clicked="[[_createNodeClickedHandler()]]"
            force-expand-and-check-node-name="[[_forceExpandAndCheckNodeName]]"
            force-expand-node-name="[[_forceExpandNodeName]]"
          >
          </tf-op-selector>
        </div>
        <div
          id="source-code-view-div"
          class="source-code-view-div"
          hidden$="{{!_sourceCodeShown}}"
        >
          <div class="debugger-section-title">Source Code</div>
          <tf-source-code-view
            id="sourceCodeView"
            request-manager="[[_requestManager]]"
            debug-watches="[[_debugWatches]]"
            focus-node-name="[[_sourceFocusNodeName]]"
            node-clicked="[[_createNodeClickedHandler()]]"
            continue-to-node="[[_createContinueToNodeHandler()]]"
          ></tf-source-code-view>
        </div>
        <tf-debugger-resizer
          current-length="{{_leftPaneWidth}}"
          min-length="[[_minleftPaneWidth]]"
          max-length="[[_maxleftPaneWidth]]"
        >
        </tf-debugger-resizer>
        <div>
          <tf-session-runs-view
            id="sessionRunsView"
            latest-session-run="[[_latestSessionRun]]"
            session-run-key-to-device-names="[[_sessionRunKey2DeviceNames]]"
            sole-active="[[_sessionRunSoleActive]]"
            node-or-tensor-clicked="[[_createFeedFetchTargetClickedHandler()]]"
          >
          </tf-session-runs-view>
        </div>
        <div class="buttons-container">
          <paper-button raised class="continue-button" on-click="_step">
            <span>[[_stepButtonText]]</span>
          </paper-button>
          <tf-debugger-continue-dialog
            id="continueDialog"
            session-run-go="[[_createSessionRunGo()]]"
            tensor-condition-go="[[_createTensorConditionGo()]]"
            force-continuation-stop="[[_createForceContinuationStop()]]"
          >
          </tf-debugger-continue-dialog>
        </div>
        <div class="container">
          <tf-graph-loader
            id="loader"
            out-graph-hierarchy="{{graphHierarchy}}"
            out-graph="{{graph}}"
            out-stats="{{stats}}"
            progress="{{_graphProgress}}"
          ></tf-graph-loader>
        </div>
      </div>
      <div class="center" slot="center" id="center-content">
        <div id="top-right-quadrant">
          <paper-tabs selected="0" selected="{{_topRightSelected}}">
            <template is="dom-repeat" items="[[_topRightTabs]]">
              <paper-tab id="[[item.id]]">[[item.name]]</paper-tab>
            </template>
          </paper-tabs>
          <div class="runtime-graph-device">
            <span id="runtime-graph-device-name"> </span>
            <paper-dropdown-menu
              id="active-runtime-graph-device-name"
              no-label-float="true"
              label="Device name"
              selected-item-label="{{_activeRuntimeGraphDeviceName}}"
            >
              <paper-listbox class="dropdown-content" slot="dropdown-content">
                <template is="dom-repeat" items="[[_activeSessionRunDevices]]">
                  <paper-item no-label-float="true">[[item]]</paper-item>
                </template>
              </paper-listbox>
            </paper-dropdown-menu>
            <paper-spinner-lite
              class="spinner"
              id="top-right-spinner"
              hidden="[[!_busy]]"
              active="[[_busy]]"
            >
            </paper-spinner-lite>
          </div>
          <paper-progress
            id="top-right-progress-bar"
            value="0"
          ></paper-progress>
          <template is="dom-if" if="[[_isTopRightRuntimeGraphsActive]]">
            <div id="graph-container">
              <tf-graph
                id="graph"
                graph-hierarchy="[[graphHierarchy]]"
                basic-graph="[[graph]]"
                stats="[[stats]]"
                progress="{{_graphProgress}}"
                color-by="structure"
                color-by-params="{{colorByParams}}"
                render-hierarchy="{{_renderHierarchy}}"
                node-context-menu-items="[[_createNodeContextMenuItems()]]"
              ></tf-graph>
              <div class="context-menu"></div>
            </div>
          </template>
          <template is="dom-if" if="[[_isTopRightTensorValuesActive]]">
            <tf-tensor-value-multi-view
              id="tensorValueMultiView"
              continue-to-callback="[[_createContinueToCallback()]]"
              tensor-name-clicked="[[_createNodeClickedHandler()]]"
              get-health-pill="[[_createGetHealthPill()]]"
            >
            </tf-tensor-value-multi-view>
          </template>
        </div>

        <tf-debugger-resizer
          is-horizontal="true"
          current-length="{{_topRightQuadrantHeight}}"
          min-length="[[_minTopRightQuadrantHeight]]"
          max-length="[[_maxTopRightQuadrantHeight]]"
        >
        </tf-debugger-resizer>

        <div id="tensor-data" class="tensor-data">
          <tf-tensor-data-summary
            id="tensorDataSummary"
            latest-tensor-data="[[_latestTensorData]]"
            expand-handler="[[_createTensorDataExpandHandler()]]"
            continue-to-callback="[[_createContinueToCallback()]]"
            highlighted-node-name="[[_highlightNodeName]]"
            tensor-name-clicked="[[_createNodeClickedHandler()]]"
            get-health-pill="[[_createGetHealthPill()]]"
          >
          </tf-tensor-data-summary>
        </div>
      </div>
    </tf-dashboard-layout>

    <style include="dashboard-style"></style>
    <style>
      :host {
        display: block;
        position: absolute;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        overflow: hidden;
      }
      paper-toast {
        text-align: center;
        font-size: 110%;
        width: 40vw;
        margin-left: 30vw;
      }
      tf-dashboard-layout {
        --tf-dashboard-layout-sidebar-basis: auto;
        --tf-dashboard-layout-sidebar-max-width: none;
        --tf-dashboard-layout-sidebar-min-width: none;
      }
      .debugger-section-title {
        font-size: 110%;
        font-weight: bold;
      }
      paper-tabs {
        color: #555;
        font-weight: normal;
      }
      paper-tab.iron-selected {
        color: black;
        font-weight: bold;
      }
      #initialDialog {
        /** This matches the default z-index of paper-dialog backdrops. */
        z-index: 102;
      }
      /** Resize the region for the graph as the user resizes the region. */
      #graph-container {
        height: calc(100% - 120px);
        /** Clip the minimap if the height of the graph container is small. */
        overflow: hidden;
        position: relative;
      }
      #graph {
        position: relative;
        display: block;
        width: 100%;
        height: 100%;
      }
      #tooltip-sorting {
        display: flex;
        font-size: 14px;
        margin-top: 5px;
      }
      #tooltip-sorting-label {
        margin-top: 13px;
      }
      #tooltip-sorting paper-dropdown-menu {
        margin-left: 10px;
        --paper-input-container-focus-color: var(--tb-orange-strong);
        width: 105px;
      }
      #x-type-selector paper-button {
        margin: 5px 3px;
      }
      .runtime-graph-device {
        align-items: center;
        display: flex;
        flex-wrap: wrap;
      }
      #runtime-graph-device-name {
        font-size: 85%;
        word-break: break-all;
        display: inline-block;
      }
      #active-runtime-graph-device-name {
        font-size: 85%;
        width: 350px;
        display: inline-block;
      }
      #top-right-progress-bar {
        width: 100%;
        display: inline-block;
        vertical-align: middle;
      }
      .line-item {
        display: block;
        padding-top: 5px;
      }
      .no-data-warning {
        max-width: 540px;
        margin: 80px auto 0 auto;
      }
      .sidebar {
        height: 100%;
        overflow-x: visible;
        position: relative;
      }
      .center {
        position: relative;
        height: 100%;
      }
      tf-debugger-resizer {
        right: -10px;
      }
      #center-content {
        position: absolute;
        right: 0;
      }
      /** The resizer should have no space to the left of it. */
      #center-content tf-debugger-resizer[is-horizontal] {
        margin-left: -23px;
      }
      .context-menu {
        position: absolute;
        display: none;
        background-color: #e2e2e2;
        border-radius: 2px;
        font-size: 14px;
        min-width: 150px;
        border: 1px solid #d4d4d4;
      }
      .spinner {
        width: 20px;
        height: 20px;
        vertical-align: middle;
      }
      .node-entries {
        box-shadow: 3px 3px #ddd;
        box-sizing: border-box;
        height: 80%;
        overflow: auto;
        padding-left: 3px;
        padding-right: 3px;
        padding-top: 3px;
        position: relative;
        vertical-align: top;
        width: 100%;
      }
      .source-code-view-div {
        position: relative;
        height: 40%;
        width: 100%;
        vertical-align: top;
        overflow: hidden;
        padding-top: 3px;
        padding-left: 3px;
        padding-right: 3px;
        box-shadow: 3px 3px #ddd;
      }
      #sessionRunsView {
        position: relative;
        width: 100%;
        overflow: auto;
        max-height: 25vh;
      }
      .buttons-container {
        padding: 20px 0;
      }
      #tensor-data {
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        padding: 20px 0;
        margin: 0 0 20px 0;
      }
      #tensorDataSummary {
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        top: 0;
      }
      #top-right-quadrant {
        height: 66%;
        overflow: auto;
      }
      .toggle-source-code {
        margin-right: 1em;
        font-size: 80%;
        float: right;
      }
      .context-menu ul {
        list-style-type: none;
        margin: 0;
        padding: 0;
        cursor: default;
      }
      .context-menu ul li {
        padding: 4px 16px;
      }
      .context-menu ul li:hover {
        background-color: #f3913e;
        color: white;
      }

      paper-input {
        width: 200px;
      }
      .inline,
      paper-item {
        display: inline;
      }

      vz-line-chart {
        height: 300px;
        position: relative;
      }
      [hidden] {
        display: none;
      }
    </style>
  </template>
  <script src="tensor-shape-helper.js"></script>
  <script>
    const _COMPUTE_WINDOW_HEIGHT = () =>
      window.innerHeight ||
      document.documentElement.clientHeight ||
      document.body.clientHeight;
    const _COMPUTE_WINDOW_WIDTH = () =>
      window.innerWidth ||
      document.documentElement.clientWidth ||
      document.body.clientWidth;
    const _TENSORBOARD_HEADER_HEIGHT = 70;
    const _DEFAULT_LEFT_PANE_WIDTH = 450;
    const _MAIN_CONTENT_LEFT_PADDING = 8;
    const _MIN_TOP_RIGHT_QUADRANT_HEIGHT = 200;
    // Default to about half the available height.
    const _DEFAULT_TOP_RIGHT_QUADRANT_HEIGHT =
      (_COMPUTE_WINDOW_HEIGHT() - _TENSORBOARD_HEADER_HEIGHT) / 2;

    Polymer({
      is: 'tf-debugger-dashboard',
      properties: {
        _topRightTabs: {
          type: Array,
          value: [
            {id: 'tab-runtime-graphs', name: 'Runtime Graphs'},
            {id: 'tab-tensor-values', name: 'Tensor Values'},
          ],
          readonly: true,
        },
        _isTopRightRuntimeGraphsActive: {
          type: Boolean,
          value: true,
        },
        _isTopRightTensorValuesActive: {
          type: Boolean,
          value: false,
        },
        _topRightSelected: {
          type: String,
          value: '0',
          observer: '_topRightSelectedChanged',
        },

        _longPollCount: {
          type: Number,
          value: 0,
        },
        _stepButtonText: {
          type: String,
          value: 'Step',
        },
        _continueButtonText: {
          type: String,
          value: 'Continue...',
        },

        _tensorViewIdCounter: {
          type: Number,
          value: 0,
        },

        isReloadDisabled: {
          type: Boolean,
          value: true,
          readOnly: true,
        },
        alreadyStarted: {
          type: Boolean,
          value: false,
        },
        _currentSessionRunInfo: {
          type: String,
          value: null,
        },
        // Counts Session Runs in aggregation, regardless of fetch/feed/target.
        _sessionRunTotalCounter: {
          type: Number,
          value: 0,
        },
        // Counts Session Runs by their fetch/feed/target combinations.
        _sessionRunCounters: {
          type: Object,
          value: {},
        },
        // A map from Session.run key to names of devices involved..
        _sessionRunKey2DeviceNames: {
          type: Object,
          value: {},
        },
        _activeSessionRunKey: {
          // TODO(cais): Maybe deduplicate with below.
          type: String,
          value: null,
        },
        _activeSessionRunDevices: {
          type: Array,
          value: [],
        },
        _activeSessionRunNumDevices: {
          type: Number,
          value: -1,
        },
        _activeRuntimeGraphDeviceName: {
          type: String,
          value: null,
          notify: true,
        },

        _highlightNodeName: {
          type: String,
          value: null,
        },

        // What kind of entity to continue over. Possibilities:
        // {'', 'SessionRun', 'TensorCondition', 'op'}.
        // '' is the default state, i.e., a state where the UI is not continuing
        // over anything.
        _continueToType: {
          type: String,
          value: '',
        },
        _continueToCounter: {
          type: Number,
          value: 0,
        },
        // A flag used to force continuation to stop.
        _continueStop: {
          type: Boolean,
          value: false,
        },
        // The target of the continue-to action.
        // For _continueToType == 'SessionRun', this can be a number of times
        //   to run Session.run for.
        // For _continueToType == 'TensorCondition', this can be the string
        //   value for a tensor value, e.g., INF_OR_NAN.
        // For _continueToType == 'op', this can be a debug tensor name.
        _continueToTarget: {
          type: String,
          value: '',
        },
        // A counter target for the continue-to action. This can be a Session.Run index
        // if _continueToType == 'SessionRun'.
        _continueToCounterTarget: {
          type: Number,
          value: -1,
        },

        // Node names (e.g., in the Node List) to force expand and/or check the
        // checkbox for.
        _forceExpandAndCheckNodeName: String,
        _forceExpandNodeName: String,
        // Focused-on op name for source code.
        _sourceFocusNodeName: String,
        _sourceCodeShown: {
          type: Boolean,
          value: false,
          observer: '_showSourceCodeChanged',
        },

        _graphProgress: {
          type: Object,
        },

        _requestManager: {
          type: Object,
          value: () => new tf_backend.RequestManager(50),
        },

        _busy: {
          type: Boolean,
          value: false,
        },

        _leftPaneWidth: {
          type: Number,
          value: tf_storage.getNumberInitializer('_leftPaneWidth', {
            defaultValue: _DEFAULT_LEFT_PANE_WIDTH,
          }),
          observer: '_leftPaneWidthObserver',
        },

        _minleftPaneWidth: {
          type: Number,
          value: _DEFAULT_LEFT_PANE_WIDTH,
          readOnly: true,
        },

        _maxleftPaneWidth: {
          type: Number,
          computed:
            '_computeMaxleftPaneWidth(_windowWidth, _maxMainContentWidth, _resizerWidth)',
        },

        _maxMainContentWidth: {
          type: Number,
          value: 350,
          readOnly: true,
        },

        _topRightQuadrantHeight: {
          type: Number,
          value: tf_storage.getNumberInitializer('_topRightQuadrantHeight', {
            defaultValue: _DEFAULT_TOP_RIGHT_QUADRANT_HEIGHT,
          }),
          observer: '_topRightQuadrantHeightObserver',
        },

        _minTopRightQuadrantHeight: {
          type: Number,
          value: _MIN_TOP_RIGHT_QUADRANT_HEIGHT,
          readOnly: true,
        },

        _maxTopRightQuadrantHeight: {
          type: Number,
          computed:
            '_computeMaxTopRightQuadrantHeight(_windowHeight, _resizerWidth)',
        },

        _resizerWidth: {
          type: Number,
          value: 30,
          readOnly: true,
        },

        _windowWidth: Number,
        _windowHeight: Number,

        /**
         * Array of {
         *   device_name: string,
         *   node_name: string,
         *   op_type: ?,
         *   output_slot: ?,
         *   debug_op: ?,
         * }
         */
        _debugWatches: Array,

        /**
         * {
         *   feeds: Array<string>,
         *   fetches: Array<string>,
         *   targets: Array<string>,
         * }
         */
        _latestSessionRun: Object,
      },

      observers: [
        '_onActiveRuntimeGraphDeviceNameChange(_activeRuntimeGraphDeviceName)',
        '_sizeDashboardRegions(_leftPaneWidth, _topRightQuadrantHeight, _windowWidth)',
        '_graphProgressUpdated(_graphProgress)',
      ],

      ready() {
        this._handleWindowResize();
        window.addEventListener(
          'resize',
          () => {
            this._handleWindowResize();
          },
          false
        );

        this.reload();
      },

      long_poll() {
        const parameters = {
          pos: ++this._longPollCount,
        };
        let path = tf_backend.getRouter().pluginRoute('debugger', '/comm');
        path = tf_backend.addParams(path, parameters);
        // Uncomment the following line for debugging.
        // console.log('Sending long-polling request to: ', path);
        this._requestManager.request(path).then((response) => {
          const responseType = response['type'];
          const responseData = response['data'];
          if (responseType === 'meta') {
            const runKey = responseData['run_key'];
            const feedNames = runKey[0].split(',');
            const fetchNames = runKey[1].split(',');
            const targetNames = runKey[2].split(',');
            const previousRunKey = this._activeSessionRunKey;
            this.set('_activeSessionRunKey', runKey);
            this.set('_latestSessionRun', {
              feeds: feedNames,
              fetches: fetchNames,
              targets: targetNames,
            });
            this.set('_sessionRunSoleActive', true);
            if (this._sessionRunKey2DeviceNames[runKey] === undefined) {
              this._sessionRunKey2DeviceNames[runKey] = [];
              this.set('_activeSessionRunDevices', []);
            } else {
              this.set(
                '_activeSessionRunDevices',
                this._sessionRunKey2DeviceNames[runKey]
              );
            }

            const runInfo =
              'Feeds: ' +
              feedNames +
              '; Fetches: ' +
              fetchNames +
              '; Targets: ' +
              targetNames;
            this._currentSessionRunInfo = runInfo;
            if (this._sessionRunCounters.hasOwnProperty(runInfo)) {
              this._sessionRunCounters[runInfo] += 1;
            } else {
              this._sessionRunCounters[runInfo] = 1;
            }
            this._sessionRunTotalCounter++;
            this.$.initialDialog.closeDialog();

            // Do not refresh node list if we are in continue model and the
            // the Sesssion.run key hasn't changed.
            const toProcessNewSessionRun =
              !this._continueToType || !_.isEqual(previousRunKey, runKey);
            if (toProcessNewSessionRun) {
              this._processGatedGrpcDebugOps(runKey, false);
              this._announceNewSessionRun();
            }
          } else if (responseType === 'tensor') {
            const deviceName = responseData['device_name'];
            const nodeName = responseData['node_name'];
            const maybeBaseExpandedNodeName =
              responseData['maybe_base_expanded_node_name'];

            if (this._activeRuntimeGraphDeviceName != deviceName) {
              // The logic here is a workaround. When the device name needs to
              // be changed, the graph rendering often finishes so late that
              // the set selectedNode call will error out.
              // TODO(cais): Find a way to change the graph and change the
              // selectedNode in the same tensor value event.
              this.set('_activeRuntimeGraphDeviceName', deviceName);
            } else {
              if (!this._continueToType) {
                // Do not refresh graph to focus on the current node if we are
                // in continue mode.
                if (this._isTopRightRuntimeGraphsActive) {
                  this._focusOnGraphNode(deviceName, maybeBaseExpandedNodeName);
                  this.set(
                    '_forceExpandNodeName',
                    deviceName + '/' + maybeBaseExpandedNodeName
                  );
                }
              }
            }

            this.set('_sessionRunSoleActive', false);
            const tensorName = nodeName + ':' + responseData['output_slot'];
            this.set('_latestTensorData', {
              deviceName: deviceName,
              tensorName: tensorName,
              nodeName: nodeName,
              maybeBaseExpandedNodeName: maybeBaseExpandedNodeName,
              debugOp: responseData['debug_op'],
              dtype: responseData['dtype'],
              shape: responseData['shape'],
              value: responseData['values'],
            });

            this._maybeUpdateTensorValueViews(
              tensorName,
              responseData['debug_op']
            );
            this.set('_busy', false);
          } else {
            console.error('Invalid long-polling response type: ', responseType);
          }

          if (this._continueToType != null) {
            this._processContinueTo(responseType, responseData);
          }

          // Long polling loop: initialite the next long polling.
          this.long_poll();
        });
      },

      _processContinueTo(responseType, responseData) {
        // Check the flag for forced stop of continuation, e.g., due to user's
        // UI action or tensor condition being met during continuation type
        // 'TensorCondition'.
        if (this._continueStop) {
          this._clearContinueTo();
        } else {
          if (this._continueToType === 'SessionRun') {
            this._processContinueToSessionRun(responseType === 'meta');
          } else if (this._continueToType === 'TensorCondition') {
            this._step();
            // Note: _conditionalHealthPillStop() will be invoked on each
            // health pill value. If it detects a health pill that matches the
            // condition, it'll stop the continuation by setting _continueStop
            // to true.
          } else if (this._continueToType === 'op') {
            this._processContinueToOp(responseType === 'meta', responseData);
          } else if (
            this._continueToType != null &&
            this._continueToType !== ''
          ) {
            console.error('Invalid _continueToType:', this._continueToType);
          }
        }
      },

      _processContinueToSessionRun(isSessionRunBeginning) {
        if (isSessionRunBeginning) {
          this.set('_continueToCounter', this._continueToCounter + 1);
        }
        if (this._continueToCounter < this._continueToCounterTarget) {
          this._step();
        } else {
          this._clearContinueTo();
        }
      },

      _processContinueToOp(isSessionRunBeginning, responseData) {
        if (isSessionRunBeginning) {
          // A new Session.run() has started, which implies that the requested
          // tensor was not hit in the previous Session.run, after the
          // continue-to-tensor action was initiated. Notify the user as such.
          this._announceNewSessionRun();
        }
        const deviceName = responseData['device_name'];
        const maybeBaseExpandedNodeName =
          responseData['maybe_base_expanded_node_name'];
        const notBaseExpandedNodeName =
          maybeBaseExpandedNodeName == null
            ? null
            : tf_debugger_dashboard.removeNodeNameBaseExpansion(
                maybeBaseExpandedNodeName
              );
        if (
          deviceName + '/' + maybeBaseExpandedNodeName ===
            this._continueToTarget ||
          deviceName + '/' + notBaseExpandedNodeName === this._continueToTarget
        ) {
          this._clearContinueTo();
          if (this._sourceCodeShown) {
            this.set('_sourceFocusNodeName', notBaseExpandedNodeName);
          }
        } else {
          this._step();
        }
      },

      _maybeUpdateTensorValueViews(tensorName, debugOp) {
        const multiView = this.$$('#tensorValueMultiView');
        if (multiView == null) {
          return;
        }
        let anyTensorMatch = false;
        _.forEach(multiView.getViews(), (valueView) => {
          if (
            valueView.tensorName === tensorName &&
            valueView.debugOp === debugOp
          ) {
            anyTensorMatch = true;
            return false; // lodash break.
          }
        });
        if (anyTensorMatch) {
          multiView.renderTensorValues();
        }
      },

      reload() {
        if (this.alreadyStarted) {
          return;
        }

        this.set('alreadyStarted', true);
        const url = tf_backend
          .getRouter()
          .pluginRoute('debugger', '/debugger_grpc_host_port');
        this._requestManager.request(url).then((response) => {
          // TODO(cais): If the Debugger Plugin backend is not activated, display message
          // informing the user of that.
          if (response.port > 0) {
            this.$.initialDialog.openDialog(response.host, response.port);
            // Initiate long-polling.
            this.long_poll();
          } else {
            this.$.initialDialog.openDisabledDialog();
          }
        });
      },

      _showSourceCodeChanged(event) {
        if (this._sourceCodeShown) {
          this.$$('#node-entries').style.height = '40%';
          this.$.sourceCodeView.render();
        } else {
          this.$$('#node-entries').style.height = '80%';
        }
      },

      _showToast(text) {
        // TODO(cais): Move to Scrolling Message View.
        this.$.toast.setAttribute('text', text);
        this.$.toast.open();
      },
      _announceNewSessionRun() {
        this._showToast(
          'Session.run() #' + this._sessionRunTotalCounter + ' is starting.'
        );
      },

      _displayGraph(runKey, deviceName) {
        const parameters = {
          run_key: JSON.stringify(runKey),
          device_name: deviceName,
        };
        const baseUrl = '/data/plugin/debugger/debugger_graph';
        const url = tf_backend.addParams(baseUrl, parameters);
        this.$.loader.datasets = [
          {
            name: '/debugger_graph',
            path: url,
          },
        ];
        this.$.loader.set('selectedDataset', 0);
      },

      _processGatedGrpcDebugOps(runKey, pollingForFirstGraph) {
        if (pollingForFirstGraph) {
          console.log('Polling for first GraphDef for run key:', runKey);
        } else {
          this.set('_activeRuntimeGraphDeviceName', null);
        }

        const parameters = {
          mode: 'retrieve_all',
          run_key: JSON.stringify(runKey),
        };
        const baseUrl = tf_backend
          .getRouter()
          .pluginRoute('debugger', '/gated_grpc');
        const url = tf_backend.addParams(baseUrl, parameters);
        let debugWatches = [];
        this._requestManager.request(url).then((response) => {
          if (response['device_names'].length == 0) {
            if (!pollingForFirstGraph) {
              // Not even a single device / runtime GraphDef has been received
              // when the Session.run starts. This is likely a Session.run
              // controlling in-graph-distributed devices.
              // Will send a step() ACK response to unblock the first graph send.
              this._step();
              this._processGatedGrpcDebugOps(runKey, true);
            } else {
              // GraphDef still hasn't arrived. Poll again.
              this._processGatedGrpcDebugOps(runKey, true);
            }
            return;
          }

          let lastDeviceName = null;
          for (const deviceName in response['gated_grpc_tensors']) {
            if (response['gated_grpc_tensors'].hasOwnProperty(deviceName)) {
              if (
                this._sessionRunKey2DeviceNames[runKey].indexOf(deviceName) ===
                -1
              ) {
                this._sessionRunKey2DeviceNames[runKey].push(deviceName);
                this.$.sessionRunsView.updateNumDevices(
                  this._sessionRunKey2DeviceNames[runKey].length
                );
              }
              this.set(
                '_activeSessionRunDevices',
                this._sessionRunKey2DeviceNames[runKey].slice()
              );
              lastDeviceName = this._activeSessionRunDevices[
                this._activeSessionRunDevices.length - 1
              ];

              const gatedGrpcTensors =
                response['gated_grpc_tensors'][deviceName];
              for (let i = 0; i < gatedGrpcTensors.length; ++i) {
                debugWatches.push({
                  device_name: deviceName,
                  node_name: gatedGrpcTensors[i][0],
                  op_type: gatedGrpcTensors[i][1],
                  output_slot: gatedGrpcTensors[i][2],
                  debug_op: gatedGrpcTensors[i][3],
                });
              }
            }
          }

          if (lastDeviceName != null) {
            this.set('_activeRuntimeGraphDeviceName', lastDeviceName);
            // TODO(cais): Make automatic updating of the selected device name work?
            const dropdownMenu = Polymer.dom(
              this.$$('#active-runtime-graph-device-name')
            );
            if (dropdownMenu != null) {
              dropdownMenu.setAttribute('selected', lastDeviceName);
            }
          }

          // Sort the debug watches and maybe base-expand the leaf nodes.
          tf_debugger_dashboard.sortAndBaseExpandDebugWatches(debugWatches);
          this.set('_debugWatches', debugWatches);
          this.$.sourceCodeView.render(debugWatches); // TODO(cais): Is debugWatches necessary?
        });
      },
      _createDebugWatchChangeHandler() {
        // This method returns a handler instead of having this method being
        // directly passed to the property binding. This is because the handler
        // references other properties of this component, and so the execution
        // context of the function (this) must be bound to the dashboard.
        return (debugObject, checked) => {
          const state = checked ? 'break' : 'disable';
          // TODO(cais): Investigate why this is fired twice sometimes.
          this._requestBreakpointStateChange(
            tf_debugger_dashboard.getCleanNodeName(
              debugObject.device_name + '/' + debugObject.node_name
            ),
            debugObject.output_slot,
            debugObject.debug_op,
            state
          );
        };
      },
      _focusOnGraphNode(deviceName, nodeName) {
        if (
          deviceName != null &&
          this._activeRuntimeGraphDeviceName !== deviceName
        ) {
          this.set('_activeRuntimeGraphDeviceName', deviceName);
        }
        this._setTopRightRuntimeGraphsToActive();
        const graph = this.$$('#graph');
        if (graph.selectedNode === nodeName) {
          // Instead of selecting the node, we pan to it - the node is already
          // selected, so selecting it would do nothing.
          graph.panToNode(nodeName);
        } else {
          // Selecting the node triggers a pan to it.
          const nodeMap = graph.get('renderHierarchy').hierarchy.getNodeMap();
          if (nodeMap[nodeName] == null) {
            // Some nodes may not be present in the graph scene, due to either
            // of the two reasons:
            //   1. A node with other names using its name as name scope may
            //      not have a base-expanded (i.e., normalized) name in the
            //      graph visualizer.
            //   2. The node may fall into a type of nodes automatically
            //      omitted by the graph visualizer, e.g., certain nodes
            //      related to TensorBoard summaries.
            // To deal with case 1, we use the non-expanded node name.
            // To deal with case 2, we check whether `nodeMap[nodeName]` equal
            // `null` again before setting `selectedNode` below.
            nodeName = tf_debugger_dashboard.removeNodeNameBaseExpansion(
              nodeName
            );
          }
          if (nodeMap[nodeName] != null) {
            graph.set('selectedNode', nodeName);
          }
        }
        this.set('_highlightNodeName', deviceName + '/' + nodeName);
      },
      _createNodeClickedHandler() {
        return (deviceName, nodeName, skipSource) => {
          // Args:
          //   deviceName: Name of the device.
          //   nodeName: Base-expanded node name.
          //   skipSource: Whether focusing on the source file line is to be skipped.
          if (this._sourceCodeShown && skipSource !== true) {
            // If the source view is active, highlight the source lines.
            this.set(
              '_sourceFocusNodeName',
              tf_debugger_dashboard.removeNodeNameBaseExpansion(nodeName)
            );
          }
          this._focusOnGraphNode(deviceName, nodeName);
          this.set('_forceExpandNodeName', deviceName + '/' + nodeName);
        };
      },
      _createFeedFetchTargetClickedHandler() {
        return (graphElementName) => {
          let nodeName = graphElementName;
          // If it is a tensor name, get the node name.
          if (nodeName.indexOf(':') !== -1) {
            nodeName = nodeName.slice(0, nodeName.indexOf(':'));
          }
          // Find the debug watch that matches the clicked node.
          const matchingDebugWatch = _.find(
            this._debugWatches,
            (debugWatch) => {
              // Take into account the possibility of base-expanded node names.
              return (
                debugWatch.node_name === nodeName ||
                (debugWatch.node_name.indexOf(nodeName) === 0 &&
                  debugWatch.node_name[nodeName.length] === '(')
              );
            }
          );
          if (matchingDebugWatch == null) {
            this._showToast(
              "Node '" +
                nodeName +
                "' is not in the runtime graph of the " +
                'current Session.run or does not have a debug op attached.'
            );
          } else {
            this._focusOnGraphNode(matchingDebugWatch.device_name, nodeName);
          }
        };
      },

      _createTensorDataExpandHandler() {
        return (tensorData) => {
          this._setTopRightTensorValuesToActive();
          // Use setTimeout() so that the DOMs in the top-right tabs have a
          // chance to update.
          setTimeout(() => {
            const multiView = this.$$('#tensorValueMultiView');
            multiView.addView({
              viewId: this._createTensorViewId(),
              deviceName: tensorData.deviceName,
              tensorName: tensorData.tensorName,
              nodeName: tensorData.nodeName,
              maybeBaseExpandedNodeName: tensorData.maybeBaseExpandedNodeName,
              debugOp: tensorData.debugOp,
              dtype: tensorData.dtype,
              shape: tensorData.shape,
              slicing: tf_debugger_dashboard.getDefaultSlicing(
                tensorData.shape
              ),
              timeIndices: '-1',
            });
          }, 10);
        };
      },

      _createTensorViewId() {
        const id = 'debugger-tensor-view-' + this._tensorViewIdCounter;
        this._tensorViewIdCounter++;
        return id;
      },

      // Create menu title and callback for the node context menus in the graph
      // visualizer.
      _createNodeContextMenuItems() {
        return [
          {
            title: (data) => {
              return 'Expand and highlight';
            },
            action: (elem, d, i) => {
              const nodeName = tf_debugger_dashboard.getCleanNodeName(
                elem.node.name
              );
              const nodeNameWithDevice =
                this._activeRuntimeGraphDeviceName + '/' + elem.node.name;
              this.set('_forceExpandNodeName', nodeNameWithDevice);
              this.set('_highlightNodeName', nodeNameWithDevice);
              if (this._sourceCodeShown) {
                this.set(
                  '_sourceFocusNodeName',
                  tf_debugger_dashboard.removeNodeNameBaseExpansion(nodeName)
                );
              }
            },
          },
          {
            title: (data) => {
              return 'Add breakpoint';
            },
            action: (elem, d, i) => {
              const nodeName = tf_debugger_dashboard.getCleanNodeName(
                elem.node.name
              );
              this.set(
                '_forceExpandAndCheckNodeName',
                this._activeRuntimeGraphDeviceName + '/' + elem.node.name
              );
              if (this._sourceCodeShown) {
                this.set(
                  '_sourceFocusNodeName',
                  tf_debugger_dashboard.removeNodeNameBaseExpansion(nodeName)
                );
              }
            },
          },
          {
            title: (data) => {
              return 'Continue to';
            },
            action: (elem, d, i) => {
              if (['_Arg', '_Retval'].indexOf(elem.node.op) !== -1) {
                this._showToast(
                  'Cannot continue to node "' +
                    elem.node.name +
                    '", due to op type "' +
                    elem.node.op +
                    '".'
                );
                return;
              }
              this._continueToNode(
                this._activeRuntimeGraphDeviceName,
                elem.node.name
              );
            },
          },
        ];
      },

      // Create a function that gives the health pill of a tensor.
      _createGetHealthPill() {
        return (watchKey, deviceName, maybeBaseExpandedNodeName, callback) => {
          const parameters = {
            watch_key: watchKey,
            time_indices: '-1', // '-1' means the latest time point.
            mapping: 'health-pill',
          };
          const baseUrl = tf_backend
            .getRouter()
            .pluginRoute('debugger', '/tensor_data');
          const url = tf_backend.addParams(baseUrl, parameters);
          this._requestManager.request(url).then((response) => {
            const healthPill = response.tensor_data[0];
            callback(healthPill);
            this._conditionalHealthPillStop(
              watchKey,
              deviceName,
              maybeBaseExpandedNodeName,
              healthPill
            );
          });
        };
      },

      // Under 'TensorCondition' continuation type, check health pills and stop
      // when they meet the given condition.
      _conditionalHealthPillStop(
        watchKey,
        deviceName,
        maybeBaseExpandedNodeName,
        healthPill
      ) {
        if (this._continueToType !== 'TensorCondition') {
          return;
        }
        if (
          tf_debugger_dashboard.checkHealthPillAgainstTensorConditionKey(
            this._continueToTarget,
            healthPill,
            this._continueToCounterTarget
          )
        ) {
          this.set('_continueStop', true);
          const nodeName = tf_debugger_dashboard.removeNodeNameBaseExpansion(
            maybeBaseExpandedNodeName
          );
          if (this._sourceCodeShown) {
            // If the source view is active, highlight the source lines.
            this.set('_sourceFocusNodeName', nodeName);
          }
          this._focusOnGraphNode(deviceName, maybeBaseExpandedNodeName);
          const nodeNameWithDeviceName =
            deviceName + '/' + maybeBaseExpandedNodeName;
          this.set('_forceExpandNodeName', nodeNameWithDeviceName);
          // Wait a little for any concurrent ongoing row rendering in the
          // Tensor Value Overview table to settle before focusing on the
          // node that just met the tensor condition.
          setTimeout(() => {
            this.set('_highlightNodeName', null);
            this.set('_highlightNodeName', nodeNameWithDeviceName);
          }, 100);
          this._showToast(
            'Tensor condition "' +
              this._continueToTarget +
              '" is met by watch key: "' +
              watchKey +
              '".\n' +
              'Stopping continuation.'
          );
        }
      },

      _continueToNode(deviceName, baseExpandedNodeName) {
        const cleanNodeName = tf_debugger_dashboard.getCleanNodeName(
          baseExpandedNodeName
        );
        const nodeNameWithDevice = deviceName + '/' + baseExpandedNodeName;
        this._requestBreakpointStateChange(
          cleanNodeName,
          0,
          'DebugIdentity',
          'break'
        );
        this.set('_forceExpandAndCheckNodeName', nodeNameWithDevice);
        if (this._sourceCodeShown) {
          this.set(
            '_sourceFocusNodeName',
            tf_debugger_dashboard.removeNodeNameBaseExpansion(cleanNodeName)
          );
        }
        this._setContinueTo('op', nodeNameWithDevice);
        this.$.continueDialog.updateContinueButtonText(true);
        this._step();
      },

      _createContinueToNodeHandler() {
        return (deviceName, baseExpandedNodeName) => {
          this._continueToNode(deviceName, baseExpandedNodeName);
        };
      },

      _onActiveRuntimeGraphDeviceNameChange(deviceName) {
        const deviceNameElement = Polymer.dom(
          this.$$('#runtime-graph-device-name')
        );
        if (this._activeSessionRunDevices.length > 0) {
          let deviceGraphLabel = deviceName;
          deviceGraphLabel +=
            ' (device ' +
            (this._activeSessionRunDevices.indexOf(deviceName) + 1) +
            ' of ' +
            this._activeSessionRunDevices.length +
            ')';
          if (this._isTopRightRuntimeGraphsActive) {
            if (deviceNameElement != null) {
              deviceNameElement.textContent = deviceGraphLabel;
            }
          }
        } else {
          if (this._isTopRightRuntimeGraphsActive) {
            if (deviceNameElement != null) {
              deviceNameElement.textContent = 'Waiting for device...';
            }
          }
        }
        if (deviceName != null) {
          this._displayGraph(this._activeSessionRunKey, deviceName);
        }
      },

      _step() {
        // Do nothing if we are currently not in a Session.run.
        if (this._activeSessionRunKey == null) {
          return;
        }
        this.set('_busy', true);

        // First, check to see if any new device GraphDef(s) have arrived and if
        // so, retrieve them.
        const parameters = {
          mode: 'retrieve_device_names',
          run_key: JSON.stringify(this._activeSessionRunKey),
        };
        const baseUrl = tf_backend
          .getRouter()
          .pluginRoute('debugger', '/gated_grpc');
        const url = tf_backend.addParams(baseUrl, parameters);
        let debugWatches = [];
        this._requestManager.request(url).then((response) => {
          let anyNewDevices = false;
          for (let i = 0; i < response['device_names'].length; ++i) {
            const deviceName = response['device_names'][i];
            if (this._activeSessionRunDevices.indexOf(deviceName) === -1) {
              anyNewDevices = true;
              break;
            }
          }
          const url = tf_backend.getRouter().pluginRoute('debugger', '/ack');
          this._requestManager.request(url).then((response) => {
            if (anyNewDevices) {
              // If there are new device(s), display them in the graph
              // visualizer and the node list, to allow user to select
              // breakpoints.
              this._processGatedGrpcDebugOps(this._activeSessionRunKey, false);
            }
          });
        });
      },

      // Create a callback for the Session.runs Go button of the continue
      // dialog.
      _createSessionRunGo() {
        return (continueNum) => {
          this._setContinueTo(
            'SessionRun',
            this._currentSessionRunInfo,
            continueNum
          );
          this._step();
        };
      },

      // Create a callback for the tensor condition Go button of the continue
      // dialog.
      _createTensorConditionGo() {
        return (conditionKey, refValue) => {
          this._setContinueTo('TensorCondition', conditionKey, refValue);
          // Force enable health pills during TensorCondition continuation.
          this.$.tensorDataSummary.enableHealthPills();
          this._step();
        };
      },

      // Create a callback for the continuation-force-stop button.
      _createForceContinuationStop() {
        return () => {
          this._showToast(
            'Continuation of type "' +
              this._continueToType +
              '" was interrupted by user.'
          );
          this.set('_continueStop', true);
        };
      },

      /**
       * @param {string} type
       * @param {string} target
       * @param {number=} counterTarget
       */
      _setContinueTo(type, target, counterTarget = -1) {
        this._continueToType = type;
        this._continueToTarget = target;
        this._continueToCounterTarget = counterTarget;
        this._continueToCounter = 0;
        this._continueStop = false;
      },
      _clearContinueTo() {
        this.$.continueDialog.notifyContinuationStop();
        this._continueToType = '';
        this._continueToTarget = '';
        this._continueToCounterTarget = -1;
        this._continueToCounter = 0;
        this._continueStop = false;
        this.set('_busy', false);
      },

      _createContinueToCallback() {
        return (deviceName, maybeBaseExpandedNodeName) => {
          const nodeNameWithDevice =
            deviceName + '/' + maybeBaseExpandedNodeName;
          this._setContinueTo('op', nodeNameWithDevice);
          this._step();
          if (this._isTopRightRuntimeGraphsActive) {
            this._focusOnGraphNode(deviceName, maybeBaseExpandedNodeName);
          }
          this.set(
            '_forceExpandNodeName',
            deviceName + '/' + maybeBaseExpandedNodeName
          );
        };
      },

      _topRightSelectedChanged(topRightSelected) {
        const selectedId = this._topRightTabs[topRightSelected].id;
        this.set(
          '_isTopRightRuntimeGraphsActive',
          selectedId === 'tab-runtime-graphs'
        );
        this.set(
          '_isTopRightTensorValuesActive',
          selectedId === 'tab-tensor-values'
        );
      },
      _setTopRightRuntimeGraphsToActive() {
        this.set('_topRightSelected', '0');
        this.set('_isTopRightRuntimeGraphsActive', true);
        this.set('_isTopRightTensorValuesActive', false);
      },
      _setTopRightTensorValuesToActive() {
        this.set('_topRightSelected', '1');
        this.set('_isTopRightRuntimeGraphsActive', false);
        this.set('_isTopRightTensorValuesActive', true);
      },

      /**
       * Request a state change for a breakpoint associated with a debugged tensor.
       * @param nodeName Name of the node that produces the tensor.
       * @param outputSlot Output slot of the tensor on the node.
       * @param debugOp Name of the debug op attached to the debugged tensor, e.g.,
       *   'DebugIdentity'.
       * @param newState New state, e.g., 'disable', 'break'.
       */
      _requestBreakpointStateChange(nodeName, outputSlot, debugOp, newState) {
        // TODO(cais): Add deviceName to args, to handle cases in which there are
        // duplicate node names across devices (rare).
        const parameters = {
          mode: 'set_state',
          node_name: nodeName,
          output_slot: outputSlot,
          debug_op: debugOp,
          state: newState,
        };
        const baseUrl = tf_backend
          .getRouter()
          .pluginRoute('debugger', '/gated_grpc');
        const url = tf_backend.addParams(baseUrl, parameters);
        this.set('_busy', true);
        this._requestManager.request(url).then((response) => {
          this.set('_busy', false);
          console.log('Breakpoint set_state response: ', response);
        });
      },

      _graphProgressUpdated(progress) {
        const topRightProgressBar = this.$$('#top-right-progress-bar');
        if (this._latestSessionRun == null) {
          // No Session.run() has connected yet. Do not show the spinner(s) or
          // the progress bar(s).
          topRightProgressBar.setAttribute('value', 0);
          this.set('_busy', false);
        } else {
          topRightProgressBar.setAttribute('value', progress.value);
          this.set('_busy', progress.value < 100);
        }
      },
      _handleWindowResize() {
        this.set('_windowWidth', _COMPUTE_WINDOW_WIDTH());
        this.set('_windowHeight', _COMPUTE_WINDOW_HEIGHT());

        this._sizeDashboardRegions(
          this._leftPaneWidth,
          this._topRightQuadrantHeight,
          this._windowWidth
        );
      },
      _computeMaxleftPaneWidth(windowWidth, maxMainContentWidth, resizerWidth) {
        // Do not let the main content exceed this width when the user
        // resizes the left panel. Otherwise, the resizer would go off
        // the viewport to the right, and the user would no longer be
        // able to access the resizer.
        return windowWidth - maxMainContentWidth - resizerWidth;
      },
      _computeMaxTopRightQuadrantHeight(windowHeight, resizerWidth) {
        // Likewise, do not let the explore container exceed this height.
        return windowHeight - resizerWidth - _TENSORBOARD_HEADER_HEIGHT;
      },
      _sizeDashboardRegions(
        leftPaneWidth,
        topRightQuadrantHeight,
        windowWidth
      ) {
        this.$$('#left-pane').style.width = leftPaneWidth + 'px';
        const contentWidth =
          windowWidth -
          leftPaneWidth -
          this._resizerWidth -
          _MAIN_CONTENT_LEFT_PADDING;
        this.$$('#center-content').style.width = contentWidth + 'px';
        const height = topRightQuadrantHeight - this._resizerWidth;
        this.$$('#top-right-quadrant').style.height = height + 'px';
        this.$$('#tensor-data').style.top = topRightQuadrantHeight + 'px';
      },
      _leftPaneWidthObserver: tf_storage.getNumberObserver('_leftPaneWidth', {
        defaultValue: _DEFAULT_LEFT_PANE_WIDTH,
      }),
      _topRightQuadrantHeightObserver: tf_storage.getNumberObserver(
        '_topRightQuadrantHeight',
        {defaultValue: _DEFAULT_TOP_RIGHT_QUADRANT_HEIGHT}
      ),
    });
  </script>
</dom-module>
